#include "NodeService.h"

#include <vector>
#include <string>
#include <iostream>
#include <sstream>

#include <getopt.h>
#include <stdarg.h>
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>

#include <transport/TSocket.h>
#include <transport/TBufferTransports.h>
#include <protocol/TBinaryProtocol.h>
#include "NodeService_client.h"

using boost::shared_ptr;

using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using namespace mp2;
using namespace std;

int m = -1;
int startingPort = -1;
int attachToNode = -1;
int stabilizeInterval = -1;
int fixInterval = -1;
int introducerPort = 2345;
const char *logconffile = NULL;
bool attached;
int defaultstart = 10000;

NodeClient * client;

void getArguments(int argc, char **argv);
int getOpenPort(int start);
void runCommands();
void parseCommand(string command);
int launchIntroducer();
int addNode(int id);
bool checkPortOpen(int port);

/**
 * Decides whether to launch and introducer or
 * attach to a client, then waits for commands.
 * @param argc Number of arguments passed in by user
 * @param argv Arguments passed in by user
 * @return 0
 */
int main(int argc, char **argv)
{
	getArguments(argc, argv);

	//If we weren't given a node to attach to
	//launch and attach to the introducer
	if (attachToNode == -1)
	{
		introducerPort = addNode(0);
		NodeInfo info;
		info.id = 0;
		info.port = introducerPort;
		sleep(5);
		client = new NodeClient(info);
		attached = false;
	}
	//Otherwise, attach to the node
	else
	{
		NodeInfo info;
		info.port = attachToNode;
		client = new NodeClient(info);
		attached = true;
	}
	runCommands();
	//won't ever happen, but whatever
	delete client;
	return 0;
}

/**
 * Adds a node to the chord network, based on
 * the global arguments read by the main function.
 * @param id ID of node to add
 * @return The port the node is listening on
 */
int addNode(int id)
{
	int port = getOpenPort(startingPort);
	if (fixInterval <= 0)
	{
		fixInterval = 3;
	}
	if (stabilizeInterval <= 0)
	{
		stabilizeInterval = 3;
	}
	char buf[1024];
	sprintf(buf,
		"xterm -e ./node --m=%i --id=%i --port=%i --stabilizeInterval=%i --fixInterval=%i "
		, m, id, port, stabilizeInterval, fixInterval);
	if (logconffile != NULL)
	{
		char temp[128];
		sprintf(temp, " --logConf=%s ", logconffile);
		strcat(buf, temp);
	}
	if (introducerPort >= 0 && id != 0)
	{
		char temp[128];
		sprintf(temp, " --introducerPort=%i ", introducerPort);
		strcat(buf, temp);
	}
	pid_t child = fork();
	if (child == 0)
	{
		system(buf);
		exit(0);
	}
	return port;
}

/**
 * Takes in commands by the user and parses them.
 */
void runCommands()
{
	string command;
	for (;;)
	{
		getline(cin, command);
		parseCommand(command);
	}
}

/**
 * Parses a command given by the user and
 * executes the appropriate action.
 * @param command Command to parse
 */
void parseCommand(string command)
{
	string token;
	stringstream stream(command);

	stream >> token;

	if (!token.compare("ADD_NODE") && !attached)
	{
		int id;
		while (stream >> id)
		{
			addNode(id);
		}
	}
	else if (!token.compare("ADD_FILE"))
	{
		string name, data;
		stream >> name;
		stream >> data;

		string temp;
		while (stream >> temp)
		{
			data.append(" " + temp);
		}
		string output;
		client->client->addFile(output, data, name);
		cout << output;
	}
	else if (!token.compare("DEL_FILE"))
	{
		string file;
		stream >> file;
		string output;
		client->client->deleteFile(output, file);
		cout << output;
	}
	else if (!token.compare("GET_FILE"))
	{
		string file, data;
		stream >> file;
		GetFileReturn r;
		client->client->getFile(r, file);
		cout << r.output;
	}
	else if (!token.compare("GET_TABLE"))
	{
		int nodeId;
		stream >> nodeId;
		NodeInfo tNode;
		client->client->getNode(tNode, nodeId);
		string output;
		client->client->getTable(output, tNode);
		cout << output;
	}
	else if (!token.compare("LIST_NODES") && !attached)
	{
		client->client->listNodes(0);
	}
	else
	{
		cout << "Invalid Command: " << token << endl;
	}
}

/**
 * Parses the command lines arguments into
 * variables used by later functions.
 * @param argc Number of arguments
 * @param argv Arguments
 */
void getArguments(int argc, char **argv)
{
	int opt;
	int long_index;

	struct option long_options[] = {
		//required
		{"m", required_argument, 0, 1000},
		//optional
		{"logConf", required_argument, 0, 1001},
		{"stabilizeInterval", required_argument, 0, 1002},
		{"fixInterval", required_argument, 0, 1003},
		{"startingPort", required_argument, 0, 1004},
		{"attachToNode", required_argument, 0, 1005},

		{0, 0, 0, 0},
	};

	while ((opt = getopt_long(argc, argv, "", long_options, &long_index)) != -1)
	{
		switch (opt)
		{
			case 0:
				if (long_options[long_index].flag != 0)
				{
					break;
				}
				printf("option %s ", long_options[long_index].name);
				if (optarg)
				{
					printf("with arg %s\n", optarg);
				}
				printf("\n");
				break;

			case 1000:
				m = strtol(optarg, NULL, 10);
				assert((m >= 3) && (m <= 10));
				break;

			case 1001:
				logconffile = optarg;
				break;

			case 1002:
				stabilizeInterval = strtol(optarg, NULL, 10);
				;
				break;

			case 1003:
				fixInterval = strtol(optarg, NULL, 10);
				;
				break;

			case 1004:
				startingPort = strtol(optarg, NULL, 10);
				assert(startingPort > 0);
				break;

			case 1005:
				attachToNode = strtol(optarg, NULL, 10);
				assert(attachToNode > 0);
				break;

			default:
				exit(1);
		}
	}

	assert((m >= 3) && (m <= 10));
}

/**
 * Finds an open port to connect to.
 * @param start The first port to try
 * @return The open port that was found
 */
int getOpenPort(int start)
{
	if (start <= 0)
	{
		start = defaultstart;
		defaultstart+=50;
	}
	for (;; start++)
	{
		if (checkPortOpen(start))
		{
			return start;
		}
		//Hopefully one of them is open. :/
		if (start >= 65534)
		{
			return -1;
		}
	}
}

bool checkPortOpen(int port)
{
	char buf[1024];
	sprintf(buf, "netstat -nan | grep %i", port);
	FILE * fp = popen(buf, "r");

	if (!fp)
	{
		return false;
	}

	char *line = fgets(buf, sizeof (buf), fp);
	pclose(fp);
	if (line == NULL || strlen(line) == 0)
	{
		return true;
	}
	return false;
}
